package com.sleazyweasel.applescriptifier;
import com.sleazyweasel.applescriptifier.preferences.MuseControllerPreferences;
import com.sleazyweasel.pandora.JsonPandoraRadio;
import com.sleazyweasel.pandora.PandoraRadio;
import com.sleazyweasel.pandora.Song;
import com.sleazyweasel.pandora.Station;
import javazoom.jlgui.basicplayer.*;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JavaPandoraPlayer implements MusicPlayer, BasicPlayerListener {
public static class MusePlayer extends BasicPlayer {
}
private static final Logger logger = Logger.getLogger(JavaPandoraPlayer.class.getName());
private PandoraRadio pandoraRadio;
private List<Station> stations;
private Station station;
private Song song;
private Song[] playlist;
private int currentSongPointer = -1;
private MusePlayer player;
private List<MusicPlayerStateChangeListener> listeners = new ArrayList<MusicPlayerStateChangeListener>();
private int currentTime;
private long totalTime;
private double volume = 0.5d;
private MusicPlayerInputType currentInputType = MusicPlayerInputType.CHOOSE_STATION;
private final MuseControllerPreferences preferences;
public JavaPandoraPlayer(MuseControllerPreferences preferences) {
this.preferences = preferences;
}
@Override
public void volumeUp() {
volume = volume + 0.1d;
if (volume > 1.0d) {
volume = 1.0d;
}
applyGain();
}
@Override
public void volumeDown() {
volume = volume - 0.1d;
if (volume < 0.0d) {
volume = 0.0d;
}
applyGain();
}
@Override
public void setVolume(double volume) {
this.volume = volume;
applyGain();
}
private void applyGain() {
try {
if (player != null && player.hasGainControl()) {
player.setGain(volume);
}
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught: " + e.getMessage(), e.getCause());
}
preferences.setPandoraVolume(volume);
notifyListeners();
}
@Override
public void close() {
try {
if (!isStopped()) {
player.stop();
}
pandoraRadio.disconnect();
pandoraRadio = null;
player = null;
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
}
@Override
public void bounce() {
boolean playing = isPlaying();
try {
if (!isStopped()) {
player.stop();
}
pandoraRadio.disconnect();
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
player = null;
pandoraRadio = null;
activate();
station = pandoraRadio.getStationById(station.getId());
refreshPlaylist();
if (playing) {
playPause();
}
notifyListeners();
}
@Override
public void activate() {
if (pandoraRadio != null && player != null) {
return;
}
player = new MusePlayer();
player.addBasicPlayerListener(this);
pandoraRadio = new JsonPandoraRadio();
logger.info("player.getStatus() = " + player.getStatus());
try {
LoginInfo loginInfo = getLogin();
pandoraRadio.sync();
pandoraRadio.connect(loginInfo.userName, loginInfo.password);
stations = sort(pandoraRadio.getStations());
notifyListeners();
} catch (BadPandoraPasswordException b) {
pandoraRadio = null;
throw b;
} catch (IOException e) {
pandoraRadio = null;
logger.log(Level.WARNING, "Exception caught.", e);
throw new RuntimeException("Failed to log in to Pandora.", e);
}
}
private List<Station> sort(List<Station> stations) {
List<Station> toBeSorted = new ArrayList<Station>(stations);
Collections.sort(toBeSorted, new Comparator<Station>() {
@Override
public int compare(Station o1, Station o2) {
if (o1.isQuickMix()) {
return -1;
}
return o1.getName().compareTo(o2.getName());
}
});
return toBeSorted;
}
private void notifyListeners() {
MusicPlayerState state = getState();
for (MusicPlayerStateChangeListener listener : listeners) {
try {
listener.stateChanged(this, state);
} catch (Exception e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
}
}
private void validateRadioState() {
logger.info("JavaPandoraPlayer.validateRadioState");
PandoraRadio pandoraRadio = this.pandoraRadio;
if (pandoraRadio != null && pandoraRadio.isAlive()) {
try {
pandoraRadio.getStations();
} catch (Exception e) {
//error means we've lost the connection.
pandoraRadio.disconnect();
try {
if (!isStopped()) {
player.stop();
}
} catch (BasicPlayerException e1) {
logger.log(Level.WARNING, "Exception caught:", e1);
}
player = null;
this.pandoraRadio = null;
this.song = null;
activate();
pandoraRadio = getRadio();
station = pandoraRadio.getStationById(station.getId());
refreshPlaylist();
}
}
}
private PandoraRadio getRadio() {
if (pandoraRadio == null || !pandoraRadio.isAlive()) {
activate();
}
return pandoraRadio;
}
@Override
public MusicPlayerState getState() {
PandoraRadio radio = getRadio();
if (stations == null) {
stations = sort(radio.getStations());
}
Map<Integer, String> stationData = new LinkedHashMap<Integer, String>(stations.size());
int i = 0;
for (Station station : stations) {
// logger.info("station.getName() = " + station.getName());
stationData.put(i++, station.getName());
}
String stationName = "";
if (station != null) {
stationName = station.getName();
}
boolean currentSongIsLoved = false;
String title = "";
String artist = "";
String album = "";
String albumArtUrl = "";
String detailUrl = "";
String currentTimeInTrack = "";
if (song != null) {
currentSongIsLoved = song.isLoved();
title = song.getTitle();
artist = song.getArtist();
album = song.getAlbum();
albumArtUrl = song.getAlbumCoverUrl();
detailUrl = song.getAlbumDetailURL();
//todo figure out how to get total track time.
currentTimeInTrack = formatTime(currentTime);
if (totalTime > 0) {
String totalTimeAsString = formatTime((int) (totalTime / 1000));
currentTimeInTrack += "/" + totalTimeAsString;
}
}
boolean isPlaying = isPlaying();
return new MusicPlayerState(currentSongIsLoved, title, artist, stationName, album, currentInputType, stationData, albumArtUrl, currentTimeInTrack, isPlaying, detailUrl, volume);
}
private String formatTime(int currentTime) {
int minutes = currentTime / 60;
int seconds = currentTime % 60;
String secondsString = String.valueOf(seconds);
if (secondsString.length() == 1) {
secondsString = "0" + secondsString;
}
return minutes + ":" + secondsString;
}
@Override
public void selectStation(Integer stationNumber) {
validateRadioState();
station = stations.get(stationNumber);
refreshPlaylist();
next();
currentInputType = MusicPlayerInputType.NONE;
if (station != null) {
preferences.setPandoraStationId(station.getId());
} else {
preferences.setPandoraStationId(-1);
}
notifyListeners();
}
private void refreshPlaylist() {
validateRadioState();
try {
playlist = pandoraRadio.getPlaylist(station, "mp3-hifi");
} catch (Exception e) {
logger.log(Level.WARNING, "Exception caught.", e);
try {
playlist = pandoraRadio.getPlaylist(station, "mp3");
} catch (Exception e1) {
logger.log(Level.WARNING, "Exception caught:", e1);
station = null;
notifyListeners();
throw new RuntimeException("Unable to retrieve station information from Pandora. Please contact musecontrol@gmail.com.", e1);
}
}
}
private void play(Song song) {
validateRadioState();
this.song = song;
try {
final URL url = new URL(song.getAudioUrl());
final InputStream inputStream = url.openStream();
final File tempFile = File.createTempFile("pandora", ".mp3");
tempFile.deleteOnExit();
final OutputStream bigBuffer = new FileOutputStream(tempFile);
final Object monitor = new Object();
new Thread(new Runnable() {
long totalBytes = 0;
public void run() {
try {
byte[] buf = new byte[8192];
while (true) {
int length = inputStream.read(buf);
if (length < 0) {
break;
}
totalBytes += length;
bigBuffer.write(buf, 0, length);
bigBuffer.flush();
if (totalBytes > 64000) {
synchronized (monitor) {
monitor.notify();
}
}
}
} catch (IOException e) {
logger.log(Level.WARNING, "Exception caught.", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
}
try {
AudioFileFormat format = AudioSystem.getAudioFileFormat(tempFile);
totalTime = getTimeLengthEstimation(format.properties());
} catch (Exception e) {
logger.log(Level.INFO, "skipping audio file properties due to error.", e);
}
}
}
}).start();
synchronized (monitor) {
monitor.wait();
}
player.open(tempFile);
player.play();
applyGain();
} catch (IOException ioe) {
logger.log(Level.WARNING, "Exception caught.", ioe);
//this seems to happen when the pandora servers are rejecting our URLs. maybe just try again?
next();
} catch (Exception e) {
//not sure what I can do here!?
logger.log(Level.WARNING, "Exception caught.", e);
throw new RuntimeException("Failed to play music.", e);
}
}
@Override
public void askToChooseStation() {
currentInputType = MusicPlayerInputType.CHOOSE_STATION;
getRadio().getStations();
notifyListeners();
}
//todo extract getLogin, saveConfig, getConfigFile, getConfigDirectory to a helper class and inject.
private LoginInfo getLogin() throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(getConfigFile()));
String userLine = reader.readLine();
String passwordLine = reader.readLine();
String userName = userLine.substring(5);
String password = passwordLine.substring(9);
return new LoginInfo(userName, password);
}
public void saveConfig(String username, char[] password) throws IOException {
File configDirectory = getConfigDirectory();
if (!configDirectory.exists()) {
configDirectory.mkdirs();
Runtime.getRuntime().exec(new String[]{"chmod", "700", configDirectory.getAbsolutePath()});
}
File configFile = getConfigFile();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(configFile))));
writer.write("user=" + username);
writer.newLine();
writer.write("password=" + new String(password));
writer.newLine();
writer.close();
}
private File getConfigFile() {
return new File(getConfigDirectory(), "config");
}
private static File getConfigDirectory() {
String userHome = System.getProperty("user.home");
return new File(userHome + "/.config/pandora");
}
@Override
public boolean isConfigured() {
return getConfigFile().exists();
}
@Override
public boolean isAuthorized() {
return isConfigured();
}
@Override
public boolean isPlaying() {
return player.getStatus() == BasicPlayer.PLAYING;
}
private boolean isStopped() {
return player.getStatus() == BasicPlayer.STOPPED || player.getStatus() == BasicPlayer.UNKNOWN;
}
@Override
public void addListener(MusicPlayerStateChangeListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(MusicPlayerStateChangeListener listener) {
listeners.remove(listener);
}
@Override
public void cancelStationSelection() {
currentInputType = MusicPlayerInputType.NONE;
}
@Override
public void initializeFromSavedUserState(MuseControllerPreferences preferences) {
Long stationId = preferences.getPreviousPandoraStationId();
Double volume = preferences.getPreviousPandoraVolume();
if (volume != -1) {
this.volume = volume;
}
if (stationId != -1) {
station = pandoraRadio.getStationById(stationId);
if (station != null) {
currentInputType = MusicPlayerInputType.NONE;
refreshPlaylist();
notifyListeners();
} else {
preferences.setPandoraStationId(-1);
}
}
}
@Override
public void playPause() {
try {
if (isStopped()) {
next();
} else if (isPlaying()) {
player.pause();
} else {
player.resume();
}
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught.", e);
throw new RuntimeException("failed to play/pause", e);
}
notifyListeners();
}
@Override
public void next() {
currentTime = 0;
totalTime = 0;
currentFrame = null;
frameDupeCount = 0;
if (station != null && playlist != null && playlist.length > 1) {
try {
player.stop();
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
play(nextSongToPlay());
}
logger.info("playlist = " + Arrays.toString(playlist));
}
private Song nextSongToPlay() {
++currentSongPointer;
Song songToPlay = playlist[currentSongPointer];
if (currentSongPointer == playlist.length - 1) {
refreshPlaylist();
currentSongPointer = -1;
}
return songToPlay;
}
@Override
public void previous() {
throw new UnsupportedOperationException();
}
@Override
public void thumbsUp() {
validateRadioState();
if (song != null) {
pandoraRadio.rate(song, true);
song = new Song(song, 1);
notifyListeners();
}
}
@Override
public void thumbsDown() {
validateRadioState();
if (song != null) {
pandoraRadio.rate(song, false);
}
next();
}
@Override
public void sleep() {
validateRadioState();
if (song != null) {
pandoraRadio.tired(song);
}
next();
}
@Override
public void opened(Object stream, Map properties) {
logger.info("JavaPandoraPlayer.opened");
}
private Long currentFrame = null;
private int frameDupeCount = 0;
@Override
public void progress(int bytesread, long microseconds, byte[] pcmdata, Map properties) {
Long frame = (Long) properties.get("mp3.frame");
int seconds = (int) (microseconds / 1000000);
if (currentFrame != null && currentFrame.equals(frame)) {
frameDupeCount++;
if (frameDupeCount > 50) {
logger.info("frame = " + frame);
logger.info("replacing player, nexting due to frame check.");
try {
player.stop();
} catch (BasicPlayerException e) {
logger.log(Level.WARNING, "Exception caught.", e);
}
player = new MusePlayer();
player.addBasicPlayerListener(this);
next();
}
} else {
frameDupeCount = 0;
}
boolean shouldNotify = false;
if (seconds - currentTime >= 1) {
shouldNotify = true;
}
currentFrame = frame;
currentTime = seconds;
if (shouldNotify) {
notifyListeners();
}
}
public long getTimeLengthEstimation(Map properties) {
long milliseconds = -1;
int byteslength = -1;
if (properties != null) {
if (properties.containsKey("audio.length.bytes")) {
byteslength = (Integer) properties.get("audio.length.bytes");
}
if (properties.containsKey("duration")) {
milliseconds = (int) (((Long) properties.get("duration")).longValue()) / 1000;
} else {
// Try to compute duration
int bitspersample = -1;
int channels = -1;
float samplerate = -1.0f;
int framesize = -1;
if (properties.containsKey("audio.samplesize.bits")) {
bitspersample = (Integer) properties.get("audio.samplesize.bits");
}
if (properties.containsKey("audio.channels")) {
channels = (Integer) properties.get("audio.channels");
}
if (properties.containsKey("audio.samplerate.hz")) {
samplerate = (Float) properties.get("audio.samplerate.hz");
}
if (properties.containsKey("audio.framesize.bytes")) {
framesize = (Integer) properties.get("audio.framesize.bytes");
}
if (bitspersample > 0) {
milliseconds = (int) (1000.0f * byteslength / (samplerate * channels * (bitspersample / 8)));
} else {
milliseconds = (int) (1000.0f * byteslength / (samplerate * framesize));
}
}
}
return milliseconds;
}
@Override
public void stateUpdated(BasicPlayerEvent event) {
// logger.info("JavaPandoraPlayer.stateUpdated");
// logger.info("event = " + event);
if (BasicPlayerEvent.EOM == event.getCode()) {
next();
}
}
@Override
public void setController(BasicController controller) {
logger.info("JavaPandoraPlayer.setController");
}
private class LoginInfo {
private final String userName;
private final String password;
public LoginInfo(String userName, String password) {
this.userName = userName;
this.password = password;
}
}
}